﻿/*
 * Copyright (c) 2015-2016 Constantijn Evenhuis (blueprints@const.nl)
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
using Blueprints.Resolution;
using System;
using System.Collections.Generic;
using System.Reflection;

namespace Blueprints.Defaults
{
    /// <summary>
    /// Describes an object.
    /// </summary>
    internal sealed class DefaultObjectDescriptor : IObjectDescriptor
    {
        private readonly ConstructorInfo constructor;
        private readonly DefaultTypeResolver typeResolver;
        private readonly Lazy<IReadOnlyList<IPropertyDescriptor>> requiredProperties;
        private readonly Lazy<IReadOnlyList<IPropertyDescriptor>> optionalProperties;

        /// <summary>
        /// Creates a new DefaultObjectDescriptor.
        /// </summary>
        /// <param name="typeResolver">The type resolver to use to resolve property types.</param>
        /// <param name="type">The type of object to describe.</param>
        /// <param name="includeProperties">Indicates which properties that to be included in serialization.</param>
        public DefaultObjectDescriptor(DefaultTypeResolver typeResolver, Type type, IncludeProperties includeProperties)
        {
            Type = type;
            this.typeResolver = typeResolver;

            bool canCreateInstance = true;
            IDictionary<string, PropertyInfo> remainingProperties = GetProperties(type);
            IReadOnlyList<PropertyInfo> requiredProperties;
            if (!TryGetRequiredProperties(type, remainingProperties, typeResolver.TargetType, includeProperties, out constructor, out requiredProperties))
            {
                constructor = null;
                requiredProperties = new PropertyInfo[0];
                canCreateInstance = false;
            }

            foreach (PropertyInfo property in requiredProperties)
            {
                string error;
                if (!CanCreateRequiredPropertyDescriptor(property, out error))
                {
                    constructor = null;
                    requiredProperties = new PropertyInfo[0];
                    canCreateInstance = false;
                    break;
                }
            }

            if (canCreateInstance)
            {
                foreach (PropertyInfo property in requiredProperties)
                {
                    remainingProperties.Remove(property.Name);
                }
            }

            this.requiredProperties = new Lazy<IReadOnlyList<IPropertyDescriptor>>(() =>
            {
                List<IPropertyDescriptor> descriptors = new List<IPropertyDescriptor>(requiredProperties.Count);
                foreach (PropertyInfo property in requiredProperties)
                {
                    descriptors.Add(new ReflectionPropertyDescriptor(property));
                }

                return descriptors;
            });

            optionalProperties = new Lazy<IReadOnlyList<IPropertyDescriptor>>(() =>
                GetOptionalPropertyDescriptors(typeResolver, type, remainingProperties, includeProperties));
        }

        /// <summary>
        /// Gets the type this descriptor describes.
        /// </summary>
        public Type Type
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the required properties of the object.
        /// </summary>
        /// <returns>An ordered list of required properties to supply to the constructor of the object in that order.</returns>
        public IReadOnlyList<IPropertyDescriptor> RequiredProperties
        {
            get
            {
                return requiredProperties.Value;
            }
        }

        /// <summary>
        /// Gets the optional properties of the object.
        /// </summary>
        /// <returns>A list of optional properties.</returns>
        public IReadOnlyList<IPropertyDescriptor> OptionalProperties
        {
            get
            {
                return optionalProperties.Value;
            }
        }

        /// <summary>
        /// Determines whether <paramref name="type"/> can successfully be described by a DefaultObjectDescriptor.
        /// </summary>
        /// <param name="type">The type to describe.</param>
        /// <param name="targetType">The target type of the root of the serialization graph.</param>
        /// <param name="includeProperties">The properties that are to be included in serialization.</param>
        /// <returns>True if the object can be described by a DefaultObjectDescriptor, false otherwise.</returns>
        public static bool CanCreateObjectDescriptor(Type type, Type targetType, IncludeProperties includeProperties)
        {
            IDictionary<string, PropertyInfo> properties = GetProperties(type);
            IReadOnlyList<PropertyInfo> requiredProperties;
            ConstructorInfo constructor;
            if (!TryGetRequiredProperties(type, properties, targetType, includeProperties, out constructor, out requiredProperties))
            {
                return false;
            }

            if (constructor == null)
            {
                return false;
            }

            string error;
            foreach (PropertyInfo property in requiredProperties)
            {
                if (!CanCreateRequiredPropertyDescriptor(property, out error))
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Creates an instance of the class.
        /// </summary>
        /// <param name="parameters">The values of the required properties, in the order returned by the GetRequiredProperties() method.</param>
        /// <returns>An instance of the class.</returns>
        public object CreateInstance(params object[] parameters)
        {
            if (constructor == null)
            {
                throw new BlueprintException("No matching constructor found for '" + Type.Name + "'");
            }

            return constructor.Invoke(parameters);
        }
        
        private static bool CanCreateRequiredPropertyDescriptor(PropertyInfo property, out string error)
        {
            if (property.IsDefined(typeof(BlueprintExcludeAttribute)))
            {
                error = "Property '" + property.DeclaringType.Name + '.' + property.Name + "' is defined as a constructor parameter in a [ConstructorProperties] attribute, and has an [BlueprintExclude] attribute. Remove the [BlueprintExclude] attribute or remove the property from the [ConstructorProperties] attribute.";
                return false;
            }

            if (property.GetMethod == null)
            {
                // A constructor property must have a get method.
                error = "Property '" + property.DeclaringType.Name + '.' + property.Name + "' is defined as a constructor parameter in a [ConstructorProperties] attribute, but its get method is unavailable. Implement a get method or remove the property from the [ConstructorProperties] attribute.";
                return false;
            }

            BlueprintIncludeAttribute include = property.GetCustomAttribute<BlueprintIncludeAttribute>();
            if (include != null)
            {
                // Properties with an [BlueprintInclude] attribute are allowed to use non-public get and set methods.
                if (property.GetMethod == null)
                {
                    error = "Property '" + property.DeclaringType.Name + '.' + property.Name + "' has an [BlueprintInclude] attribute, but its get method is unavailable. Remove the attribute or implement a get method for the property.";
                    return false;
                }

                error = null;
                return true;
            }

            if (!property.GetMethod.IsPublic)
            {
                // Automatically included properties should be publically accessible.
                error = "Property '" + property.DeclaringType.Name + '.' + property.Name + "' is defined as a constructor parameter in a [ConstructorProperties] attribute, but its get method is unavailable. Add an [BlueprintInclude] attribute to allow using the non-public get method, make the get method public or remove the property from the [ConstructorProperties] attribute.";
                return false;
            }

            error = null;
            return true;
        }

        private static bool TryGetMatchingConstructor(Type type, IReadOnlyList<PropertyInfo> requiredProperties, out ConstructorInfo constructor)
        {
            foreach (ConstructorInfo candidate in type.GetTypeInfo().DeclaredConstructors)
            {
                if (!candidate.IsPublic)
                {
                    continue;
                }

                if (candidate.IsStatic)
                {
                    continue;
                }

                ParameterInfo[] parameters = candidate.GetParameters();
                if (parameters.Length != requiredProperties.Count)
                {
                    continue;
                }

                bool mismatch = false;
                for (int i = 0; i < parameters.Length; i++)
                {
                    if (!parameters[i].ParameterType.GetTypeInfo().IsAssignableFrom(requiredProperties[i].PropertyType.GetTypeInfo()))
                    {
                        mismatch = true;
                        break;
                    }
                }

                if (mismatch)
                {
                    continue;
                }

                constructor = candidate;
                return true;
            }

            constructor = default(ConstructorInfo);
            return false;
        }

        private static IDictionary<string, PropertyInfo> GetProperties(Type type)
        {
            Dictionary<string, PropertyInfo> properties = new Dictionary<string, PropertyInfo>(StringComparer.Ordinal);
            foreach (PropertyInfo property in type.GetRuntimeProperties())
            {
                if (property.GetMethod == null)
                {
                    continue;
                }

                if (property.GetMethod.IsStatic)
                {
                    if (property.IsDefined(typeof(BlueprintIncludeAttribute)))
                    {
                        throw new BlueprintException("Property '" + property.DeclaringType.Name + '.' + property.Name + "' has an [IncludePropertyAttribute], but static properties are not supported.");
                    }

                    continue;
                }

                if (property.GetMethod.GetParameters().Length > 0)
                {
                    if (property.IsDefined(typeof(BlueprintIncludeAttribute)))
                    {
                        throw new BlueprintException("Property '" + property.DeclaringType.Name + '.' + property.Name + "' has an [IncludePropertyAttribute], but properties with parameters are not supported.");
                    }

                    continue;
                }

                properties[property.Name] = property;
            }

            return properties;
        }

        private static bool TryGetBlueprintConstructorAttribute(Type type, Type targetType, out BlueprintConstructorAttribute attribute)
        {
            // Allow overrides on the target type level
            foreach (BlueprintConstructorAttribute candidate in targetType.GetTypeInfo().GetCustomAttributes<BlueprintConstructorAttribute>())
            {
                if (candidate.Type == type)
                {
                    attribute = candidate;
                    return true;
                }
            }

            foreach (BlueprintConstructorAttribute candidate in type.GetTypeInfo().GetCustomAttributes<BlueprintConstructorAttribute>())
            {
                // The attribute on the type itself does not need an explicit type indication
                if (candidate.Type == null || candidate.Type == type)
                {
                    attribute = candidate;
                    return true;
                }
            }

            attribute = null;
            return false;
        }

        private static bool TryGetRequiredProperties(Type type, IDictionary<string, PropertyInfo> properties, Type targetType, IncludeProperties includeProperties, out ConstructorInfo constructor, out IReadOnlyList<PropertyInfo> requiredProperties)
        {
            BlueprintConstructorAttribute attribute;
            if (TryGetBlueprintConstructorAttribute(type, targetType, out attribute))
            {
                string[] requiredPropertyNames = attribute.ParameterProperties;

                List<PropertyInfo> required = new List<PropertyInfo>();
                for (int i = 0; i < requiredPropertyNames.Length; i++)
                {
                    PropertyInfo property;
                    if (!properties.TryGetValue(requiredPropertyNames[i], out property))
                    {
                        constructor = null;
                        requiredProperties = default(IReadOnlyList<PropertyInfo>);
                        return false;
                    }

                    required.Add(property);
                }

                requiredProperties = required;
            }
            else
            {
                requiredProperties = new PropertyInfo[0];
            }

            if (TryGetMatchingConstructor(type, requiredProperties, out constructor))
            {
                return true;
            }

            return TryGuessRequiredProperties(type, properties, includeProperties, out constructor, out requiredProperties);
        }

        private static bool TryGuessRequiredProperties(Type type, IDictionary<string, PropertyInfo> properties, IncludeProperties includeProperties, out ConstructorInfo constructor, out IReadOnlyList<PropertyInfo> requiredProperties)
        {
            TypeInfo typeInfo = type.GetTypeInfo();
            if (typeInfo.IsValueType)
            {
                if ((includeProperties & IncludeProperties.GuessStructConstructorByProperties) != IncludeProperties.GuessStructConstructorByProperties)
                {
                    constructor = null;
                    requiredProperties = null;
                    return false;
                }
            }
            else
            {
                if ((includeProperties & IncludeProperties.GuessClassConstructorByProperties) != IncludeProperties.GuessClassConstructorByProperties)
                {
                    constructor = null;
                    requiredProperties = null;
                    return false;
                }
            }

            Dictionary<string, PropertyInfo> caseInsensitive = new Dictionary<string, PropertyInfo>(properties, StringComparer.OrdinalIgnoreCase);
            foreach (ConstructorInfo candidate in typeInfo.DeclaredConstructors)
            {
                if (TryMatchProperties(candidate.GetParameters(), caseInsensitive, out requiredProperties))
                {
                    constructor = candidate;
                    return true;
                }
            }

            constructor = null;
            requiredProperties = default(IReadOnlyList<PropertyInfo>);
            return false;
        }

        private static bool TryMatchProperties(ParameterInfo[] parameters, IDictionary<string, PropertyInfo> properties, out IReadOnlyList<PropertyInfo> requiredProperties)
        {
            PropertyInfo[] matches = new PropertyInfo[parameters.Length];
            for (int i = 0; i < parameters.Length; i++)
            {
                ParameterInfo parameter = parameters[i];
                TypeInfo parameterTypeInfo = parameter.ParameterType.GetTypeInfo();
                PropertyInfo property;
                if (!properties.TryGetValue(parameter.Name, out property))
                {
                    requiredProperties = default(IReadOnlyList<PropertyInfo>);
                    return false;
                }

                if (!parameterTypeInfo.IsAssignableFrom(property.PropertyType.GetTypeInfo()))
                {
                    requiredProperties = default(IReadOnlyList<PropertyInfo>);
                    return false;
                }

                matches[i] = property;
            }

            requiredProperties = matches;
            return true;
        }

        private static string FirstLetterToUpper(string str)
        {
            if (str == null)
            {
                return null;
            }

            if (str.Length > 1)
            { 
                return char.ToUpper(str[0]) + str.Substring(1);
            }

            return str.ToUpper();
        }

        private IReadOnlyList<IPropertyDescriptor> GetOptionalPropertyDescriptors(ITypeResolver typeResolver, Type type, IDictionary<string, PropertyInfo> properties, IncludeProperties includeProperties)
        {
            List<IPropertyDescriptor> optional = new List<IPropertyDescriptor>();
            foreach (PropertyInfo property in properties.Values)
            {
                IPropertyDescriptor descriptor;
                if (!TryCreateOptionalPropertyDescriptor(property, includeProperties, out descriptor))
                {
                    continue;
                }

                optional.Add(descriptor);
            }

            return optional;
        }

        private bool TryCreateOptionalPropertyDescriptor(PropertyInfo property, IncludeProperties includeProperties, out IPropertyDescriptor descriptor)
        {
            if (property.IsDefined(typeof(BlueprintExcludeAttribute)))
            {
                // Properties with an [ExcludePropertyAttribute] should not be serialized.
                descriptor = default(IPropertyDescriptor);
                return false;
            }

            BlueprintIncludeAttribute include = property.GetCustomAttribute<BlueprintIncludeAttribute>();
            if (include != null)
            {
                // Properties with an [BlueprintInclude] attribute are allowed to use non-public get and set methods.
                if (property.GetMethod == null)
                {
                    throw new BlueprintException("Property '" + property.DeclaringType.Name + '.' + property.Name + "' has an [BlueprintInclude] attribute, but its get method is unavailable. Remove the attribute or implement a get method for the property.");
                }

                if (include.IncludeAsReadOnly || property.SetMethod == null)
                {
                    switch (typeResolver.GetTypeCategory(property.PropertyType))
                    {
                        case TypeCategory.Object:
                        case TypeCategory.NotInstantiable:
                            descriptor = new ReadOnlyObjectProperty(property, typeResolver.GetObjectDescriptor(property.PropertyType));
                            return true;
                        case TypeCategory.Collection:
                            descriptor = new ReadOnlyCollectionProperty(property, typeResolver.GetCollectionDescriptor(property.PropertyType));
                            return true;
                        default:
                            throw new BlueprintException("Property '" + property.Name + "' of type '" + property.DeclaringType.Name + "' has an [BlueprintInclude] attribute, but the property is read-only. Remove the attribute or make the property writeable.");
                    }
                }

                descriptor = new ReflectionPropertyDescriptor(property);
                return true;
            }

            if ((includeProperties & IncludeProperties.IncludePublicGetSetProperties) != IncludeProperties.IncludePublicGetSetProperties)
            {
                // Only properties with an [BlueprintInclude] attribute are to be included.
                descriptor = default(IPropertyDescriptor);
                return false;
            }

            if (property.GetMethod == null || !property.GetMethod.IsPublic)
            {
                // Automatically included properties should be publically accessible.
                descriptor = default(IPropertyDescriptor);
                return false;
            }

            if (property.SetMethod == null || !property.SetMethod.IsPublic)
            {
                if ((includeProperties & IncludeProperties.IncludePublicGetProperties) != IncludeProperties.IncludePublicGetProperties)
                {
                    // Read-only properties are not to be included without an [BlueprintInclude] attribute.
                    descriptor = default(IPropertyDescriptor);
                    return false;
                }

                switch (typeResolver.GetTypeCategory(property.PropertyType))
                {
                    case TypeCategory.Object:
                        descriptor = new ReadOnlyObjectProperty(property, typeResolver.GetObjectDescriptor(property.PropertyType));
                        return true;
                    case TypeCategory.Collection:
                        descriptor = new ReadOnlyCollectionProperty(property, typeResolver.GetCollectionDescriptor(property.PropertyType));
                        return true;
                    default:
                        descriptor = default(IPropertyDescriptor);
                        return false;
                }
            }

            descriptor = new ReflectionPropertyDescriptor(property);
            return true;
        }
    }
}